/**
 * PHOSIDE: PHosphorylation Site IDentification Engine.
 * Copyright 2009 Digital Biology Lab, University of Missouri.
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3 of the License, or (at your option) any later
 * version. <p/> This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the License for more
 * details.
 */

package phoside.data.feature.extaction;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;

import phoside.Protein;
import phoside.ProteinImpl;

import phoside.data.feature.model.Instance;
import phoside.data.feature.model.InstanceImpl;
import phoside.data.feature.model.InstanceTag;
import phoside.data.feature.model.InstanceTagImpl;

import phoside.knn.KNNExtractor;
import phoside.knn.KNNExtractorImpl;
import phoside.knn.ClosenessCalculator;
import phoside.knn.SequenceBasedClosenessCalculator;

import phoside.util.StaticFinalValues;

/**
 *
 * @author gjj
 */
public class KNNFeatureExtractor
        implements FeatureExtractor, Serializable{
    private static final long serialVersionUID = 1321845359940459986L;
    
    protected List<Instance> instances;
    protected List<Double> label;
    protected final TreeSet<Integer> Ks;
    protected final KNNExtractor knnExtractor;

    public KNNFeatureExtractor(final int[] Ks,
                               final KNNExtractor knnExtractor) {
        if (Ks==null || knnExtractor==null) {
            throw new NullPointerException();
        }

        if (Ks.length==0) {
            throw new IllegalArgumentException();
        }

        this.Ks = new TreeSet<Integer>();
        for (int K : Ks) {
            this.Ks.add(K);
        }

        this.knnExtractor = knnExtractor;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        if (knnExtractor instanceof KNNExtractorImpl) {
            ClosenessCalculator cc = ((KNNExtractorImpl)knnExtractor).getClosenessCalculator();
            if (cc instanceof SequenceBasedClosenessCalculator) {
                int windowOffset = ((SequenceBasedClosenessCalculator)cc).getWindowOffset();
                List<Instance> old = instances;
                instances = rearrageInstances(windowOffset);
                out.defaultWriteObject();
                instances = old;
            }
        } else {
            out.defaultWriteObject();
        }
    }

    // rearrange instances to save memory
    private List<Instance> rearrageInstances(int windowOffset) {
        ProteinImpl newProtein = new ProteinImpl("p",null,null,null,null);//made up protein

        int width = 2*windowOffset+1;

        int n = instances.size();
        StringBuilder newProteinSeq = new StringBuilder(n*width);
        List<Instance> newInstances = new ArrayList(n);

        for (int i=0; i<n; i++) {
            // old instance and tag
            Instance ins = instances.get(i);
            InstanceTag tag = ins.getInstanceTag();
            Object obj = tag.getTag(StaticFinalValues.INSTANCETAGNAMEPROTEIN);
            if (!(obj instanceof Protein)) {
                throw new java.lang.UnsupportedOperationException();
            }
            Protein protein = (Protein)obj;

            obj = tag.getTag(StaticFinalValues.INSTANCETAGNAMESITE);
            if (!(obj instanceof Integer)) {
                throw new java.lang.UnsupportedOperationException();
            }
            int site = (Integer)obj;

            String proSeq = protein.getSequence();

            String seq = proSeq.substring(site-windowOffset, site+windowOffset+1);

            // new seq
            newProteinSeq.append(seq);

            // new tag
            InstanceTag newTag = new InstanceTagImpl();
            newTag.addTag(StaticFinalValues.INSTANCETAGNAMEPROTEIN, newProtein);
            newTag.addTag(StaticFinalValues.INSTANCETAGNAMESITE, i*width+windowOffset);

            // new instance
            Instance newIns = new InstanceImpl(newTag);

            // TODO: do we need to copy the features? not for sequence-based knn.

            newInstances.add(newIns);
        }

        newProtein.setSequence(newProteinSeq.toString());

        return newInstances;
    }

    public void setTrainingData(final List<Instance> instances,
                                       final List<Double> label) {
        if (instances==null || label==null) {
            throw new NullPointerException();
        }

        int N = instances.size();
        if (label.size()!=N) {
            throw new java.lang.IllegalArgumentException("Each instance must have exact one label");
        }

        int minK = this.Ks.first();
        int maxK = this.Ks.last();
        if (minK<=0 || maxK>N) {
            throw new IndexOutOfBoundsException();
        }
        this.instances = instances;
        this.label = label;
    }


    /**
     * 
     * @param saveToInstance
     * @param instances
     * @param K
     */
    public List<Double> extract(final Instance instance, final boolean save) {
        if (instance==null) {
            throw new NullPointerException();
        }

        if (instances==null) {
            throw new IllegalStateException("Set training data first");
        }

        List<Double> features = new ArrayList<Double>();

        knnExtractor.getNNsSortedByDistance(instance, instances);
        List<Integer> indices = knnExtractor.getKNNIndex();
        //List<Double> distances = knnExtractor.getKNNDistance();

        int count = 0;
        int prek = 0;
        for (int k : Ks) {
            for (int i=prek; i<k;i++){
                if (label.get(indices.get(i))==1)
                    count ++ ;
            }

            features.add(count*1.0/k);
            prek = k;
        }

        if (save) {
            instance.putFeatures(featureTag(), features);
        }

        return features;
    }

    public String featureTag() {
        return StaticFinalValues.KNN;
    }
}
